﻿using System;
using System.Text;
using System.Net;
using System.IO;
using System.Collections.Generic;
using System.Globalization;
using System.Threading;
using System.Diagnostics;

namespace NeroConnect.Network
{
	/// <summary>static class with some helper methods for HTTP</summary>
	public static class HttpHelper
	{
		/// <summary>Structure which is returned by ParseHttpHeaders and which contains the information of the parsed headers</summary>
		public class HttpHeaders
		{
			/// <summary>Enumeration of the possible header types</summary>
			public enum HeaderType
			{
				/// <summary>The header information has an unknown format/type</summary>
				Unknown,
				/// <summary>The header is a request header, only Method, Path, Version and Headers are valid. StatusCode and StatusString are not set and can be ignored</summary>
				Request,
				/// <summary>The header is a response header, only Version, StatusCode, StatusString, and Headers are valid. Method and Path are not set and can be ignored</summary>
				Response
			}
			/// <summary>Defines the type of the header information. Depending on this type, specific properties are set or can be ignored</summary>
			public HeaderType Type {get;set;}
			/// <summary>HTTP version</summary>
			public Version Version { get; set; }
			/// <summary>Status code of the HTTP response. Only valid if Type is set to Response</summary>
			public HttpStatusCode StatusCode { get; set; }
			/// <summary>Status string of the HTTP response. Only valid if Type is set to Response</summary>
			public string StatusString { get; set; }
			/// <summary>Method/verb of the HTTP request. Only valid of Type is set to Request</summary>
			public string Method { get; set; }
			/// <summary>Path/URI of the HTTP request. Only valid of Type is set to Request</summary>
			public string Path { get; set; }
			/// <summary>Additional headers defined in the request/response.</summary>
			public WebHeaderCollection Headers { get; internal set; }
		}

		/// <summary>Checks if a given HTTP status code is a successful code or if it's an error code</summary>
		/// <param name="statusCode">HTTP status code to check</param>
		/// <returns>true if statusCode is a successful status code, false otherwise</returns>
		public static bool IsSuccess(this System.Net.HttpStatusCode statusCode)
		{
			return (statusCode < HttpStatusCode.BadRequest);
		}

		private static readonly Dictionary<int, string> _descriptions = new Dictionary<int, string>()
		{
			{ 100, "Continue" },
			{ 101, "Switching Protocols" },
			{ 200, "OK" },
			{ 201, "Created" },
			{ 202, "Accepted" },
			{ 203, "Non-Authoritative Information" },
			{ 204, "No Content" },
			{ 205, "Reset Content" },
			{ 206, "Partial Content" },
			{ 207, "Multi-Status" },
			{ 300, "Multiple Choices" },
			{ 301, "Moved Permanently" },
			{ 302, "Found" },
			{ 303, "See Other" },
			{ 304, "Not Modified" },
			{ 305, "Use Proxy" },
			{ 307, "Temporary Redirect" },
			{ 400, "Bad Request" },
			{ 401, "Unauthorized" },
			{ 402, "Payment Required" },
			{ 403, "Forbidden" },
			{ 404, "Not Found" },
			{ 405, "Method Not Allowed" },
			{ 406, "Not Acceptable" },
			{ 407, "Proxy Authentication Required" },
			{ 408, "Request Time-out" },
			{ 409, "Conflict" },
			{ 410, "Gone" },
			{ 411, "Length Required" },
			{ 412, "Precondition Failed" },
			{ 413, "Request Entity Too Large" },
			{ 414, "Request-URI Too Large" },
			{ 415, "Unsupported Media Type" },
			{ 416, "Requested range not satisfiable" },
			{ 417, "Expectation Failed" },
			{ 423, "Locked" },
			{ 500, "Internal Server Error" },
			{ 501, "Not Implemented" },
			{ 502, "Bad Gateway" },
			{ 503, "Service Unavailable" },
			{ 504, "Gateway Time-out" },
			{ 505, "HTTP Version not supported" },
			{ 507, "Insufficient Storage" },
		};

		/// <summary>Returns the description / status code phrase of an HTTP status code which can be used in a HTTP response</summary>
		/// <param name="statusCode">HTTP status code for which the description should be returned</param>
		/// <returns>string representation of the HTTP status code</returns>
		public static string GetDescription(this System.Net.HttpStatusCode statusCode)
		{
			int key = (int) statusCode;
			if (_descriptions.ContainsKey(key))
				return _descriptions[key];
			return "Unkown Status Code";
		}


		/// <summary>Parses a byte array for HTTP headers. This method is particularly helpful when receiving SSDP/HTTP-like packages over UDP.</summary>
		/// <param name="data">byte array to be parsed</param>
		/// <returns>The parsed http header information</returns>
		/// <exception cref="System.ArgumentNullException">data is null</exception>
		/// <exception cref="System.ArgumentException">The message is not well a formed HTTP request/response header</exception>
		public static HttpHeaders ParseHttpHeaders(byte[] data)
		{
			MemoryStream memoryStream = null;
			try
			{
				memoryStream = new MemoryStream(data);
				using (StreamReader reader = new StreamReader(memoryStream))
				{
					memoryStream = null;
					string firstLine = reader.ReadLine();
					string[] firstLineParts = firstLine.Split(" ".ToCharArray(), 3);
					if (firstLineParts.Length != 3)
						throw new ArgumentException("First line is not a well formed HTTP header");
					HttpHeaders headers = new HttpHeaders();
					if (firstLineParts[0].StartsWith("HTTP/", StringComparison.OrdinalIgnoreCase))
					{
						headers.Type = HttpHeaders.HeaderType.Response;
						headers.Version = GetHttpVersion(firstLineParts[0]);
						headers.StatusCode = (HttpStatusCode)int.Parse(firstLineParts[1], CultureInfo.InvariantCulture);
						headers.StatusString = firstLineParts[2];
					}
					else if (firstLineParts[2].StartsWith("HTTP/", StringComparison.OrdinalIgnoreCase))
					{
						headers.Type = HttpHeaders.HeaderType.Request;
						headers.Method = firstLineParts[0];
						headers.Path = firstLineParts[1];
						headers.Version = GetHttpVersion(firstLineParts[2]);
					}
					else
						throw new ArgumentException("First line is not a well formed HTTP header");
					string line = null;
					headers.Headers = new WebHeaderCollection();
					while (!string.IsNullOrEmpty(line = reader.ReadLine()))
					{
						headers.Headers.Add(line);
					}
					return headers;
				}
			}
			finally
			{
				if (memoryStream != null)
					memoryStream.Dispose();
			}
		}

		/// <summary>Creates a byte array for an HTTP request header</summary>
		/// <param name="method">Method to be used in the HTTP request</param>
		/// <param name="path">Path of the request</param>
		/// <param name="header">Addition HTTP headers</param>
		/// <returns>Byte array which represents the HTTP request header</returns>
		/// <exception cref="System.ArgumentException">method and/or path is/are null or empty</exception>
		public static byte[] CreateByteArrayForHttpRequestHeader(string method, string path, System.Net.WebHeaderCollection header)
		{
			if (string.IsNullOrEmpty(method) || string.IsNullOrEmpty(path))
				throw new ArgumentException("method and path need to be specified");
			string firstLine = method + " " + Uri.EscapeUriString(path) + " HTTP/1.1";
			return CreateByteArrayForHttpHeader(firstLine, header);
		}

		/// <summary>Creates a byte array for an HTTP response header</summary>
		/// <param name="statusCode">Status code of the response</param>
		/// <param name="header">Addition HTTP headers to be added to the result</param>
		/// <returns>Byte array which represents the HTTP response header</returns>
		public static byte[] CreateByteArrayForHttpResponseHeader(HttpStatusCode statusCode, System.Net.WebHeaderCollection header)
		{
			string firstLine = "HTTP/1.1 " + (int)statusCode + " " + statusCode.GetDescription();
			return CreateByteArrayForHttpHeader(firstLine, header);
		}

		private static byte[] CreateByteArrayForHttpHeader(string firstLine, System.Net.WebHeaderCollection header)
		{
			StringBuilder sb = new StringBuilder();
			sb.AppendLine(firstLine);
			if (header != null)
				sb.Append(header.ToString());
			else
				sb.AppendLine();
			string data = sb.ToString();
			return Encoding.ASCII.GetBytes(data);
		}

		private static Version GetHttpVersion(string httpVersionString)
		{
			string[] parts = httpVersionString.Split('/');
			if (parts.Length != 2)
				throw new ArgumentException("Incorrectly formed HTTP version string: " + httpVersionString);
			return new Version(parts[1]);
		}

        /// <summary>
        /// 
        /// </summary>
        /// <param name="request"></param>
        /// <param name="timeoutInMilliseconds"></param>
        /// <returns></returns>
		public static IResponse GetResponseAsynchronously(this IRequest request, int timeoutInMilliseconds)
		{
			AutoResetEvent getResponseFinished = new AutoResetEvent(false);
			IResponse response = null;
            Exception exception = null;

			request.BeginGetResponse(ar =>
			{
				try
				{
					response = request.EndGetResponse(ar);
				}
				catch (Exception ex)
				{
                    exception = ex;
                    Trace.TraceWarning("Exception in EndGetResponse: " + ex.Message);
				}
				finally
				{
					getResponseFinished.Set();
				}
			}, null);
			if (!getResponseFinished.WaitOne(timeoutInMilliseconds))
			{
				request.Abort();
			}

            if (exception != null)
            {
                throw exception;
            }

			return response;
		}

        /// <summary>
        /// 
        /// </summary>
        /// <param name="request"></param>
        /// <param name="timeoutInMilliseconds"></param>
        /// <returns></returns>
		public static Stream GetRequestStreamAsynchronously(this IRequest request, int timeoutInMilliseconds)
		{
			AutoResetEvent getRequestStreamFinished = new AutoResetEvent(false);
			Stream stream = null;
			request.BeginGetRequestStream(ar =>
			{
				try
				{
					stream = request.EndGetRequestStream(ar);
				}
				catch (Exception ex)
				{
					Trace.TraceWarning("Exception in EndGetRequestStream: " + ex.Message);
				}
				finally
				{
					getRequestStreamFinished.Set();
				}
			}, null);
			if (!getRequestStreamFinished.WaitOne(timeoutInMilliseconds))
			{
				request.Abort();
			}
			return stream;
		}
	}
}
